/*
 * Copyright 2008 Lucas Tan.
 * This copyright notice must always remain intact whenever you use or distribute 
 * the source code contained in this file.
 * Any usage of any code in this file, either in derived work or
 * through the use of external linkage,
 * must always be credited with the author's name.
 * 
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 * 
 * */
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using PassKeeper.Cryptography;

namespace PassKeeper.Data
{
	public sealed class Database : Element
	{
		#region Static Members

		internal const string ElementName = "Database";

		/// <summary>
		/// The key length needed to feed into the HMAC algorithm used
		/// to sign the XML document.
		/// </summary>
		private const int HMACKeyLength = 64/*bytes*/;

		private const string SignatureElementName = "Signature";
		private const string VersionAttributeName = "Version";
		
		private static readonly XmlWriterSettings s_writerSettings;
		private static readonly XmlReaderSettings s_readerSettings;

		internal static readonly UTF8Encoding s_UTF8;
	
		static Database()
		{
			/*Do not emit byte order mark (BOM) and do not
			 throw exception on invalid bytes.*/
			s_UTF8 = new UTF8Encoding(false, false);

			s_writerSettings = new XmlWriterSettings();
			s_writerSettings.Encoding = s_UTF8;
			s_writerSettings.CheckCharacters = true;
			s_writerSettings.ConformanceLevel = ConformanceLevel.Document;
			s_writerSettings.CloseOutput = true;
			s_writerSettings.Indent = true;
			s_writerSettings.IndentChars = "  "; /*two spaces*/

			/*Replace all other types of line endings such as \n or \r\n
			 to the one specified in NewLineChars*/
			s_writerSettings.NewLineHandling = NewLineHandling.Replace;
			s_writerSettings.NewLineOnAttributes = true;
			s_writerSettings.OmitXmlDeclaration = false;

			s_readerSettings = new XmlReaderSettings();
			s_readerSettings.CheckCharacters = true;
			s_readerSettings.ConformanceLevel = ConformanceLevel.Document;
			s_readerSettings.CloseInput = false;
			
			/*We need the <?xml processing instruction to tell us the encoding.*/
			s_readerSettings.IgnoreProcessingInstructions = false;

			/*Ignore comments and whitespace to speed up reading and
			 for signature validation to work.*/
			s_readerSettings.IgnoreWhitespace =true;
			s_readerSettings.IgnoreComments = true;
			s_readerSettings.ProhibitDtd = true;
			s_readerSettings.ValidationType = ValidationType.None;
		}

		#endregion

		#region Non-static Members

		private AccountCollection _accounts;
		private DatabaseVersion _version;
		private SymmetricCipher _symCipher;
		private Hasher _hasher;
		private DeriveBytes _deriveBytes;
		private byte[] _key;

		~Database()
		{
			Array.Clear(_key, 0, _key.Length);
			ParserUtil.Dispose(_symCipher);
			ParserUtil.Dispose(_hasher);
			ParserUtil.Dispose(_deriveBytes);
		}

		private Database() : base()
		{
			_accounts = new AccountCollection(this);
			_accounts.Parent = this;

			/*Default values.*/
			_hasher = new Hasher(this, HasherType.SHA512);
			_symCipher = new SymmetricCipher(this,SymmetricCipherType.AES);

			/*Set to the latest version.*/
			_version = DatabaseVersion.Version1_0;
		}

		public Database(byte[] passwordBytes):this()
		{
			SetPassword(passwordBytes);
		}

		public Database(string password):this()
		{
			SetPassword(password);
		}

		public Database(string password, Encoding encoding):this()
		{
			SetPassword(password, encoding);
		}

		public void SetPassword(byte[] passwordBytes)
		{
			SetPassword(new PKDeriveBytes(passwordBytes, PKDeriveBytes.DefaultIterationCount));
		}

		public void SetPassword(string password)
		{
			SetPassword(password, s_UTF8);
		}

		public void SetPassword(string password, Encoding encoding)
		{
			if (encoding == null)
			{
				throw new ArgumentNullException("encoding");
			}

			if (string.IsNullOrEmpty(password))
			{
				throw new ArgumentException("Cannot be null or empty.", "password");
			}

			SetPassword(encoding.GetBytes(password));
		}
	
		private void SetPassword(DeriveBytes deriveBytes)
		{
			if (deriveBytes == null)
			{
				throw new ArgumentNullException("deriveBytes");
			}

			/*Must decrypt the contents first...*/
			_accounts.EnsureDecrypted(true);

			ParserUtil.Dispose(_deriveBytes);

			/*...before the derive bytes is changed.*/
			_deriveBytes = deriveBytes;

			InvalidateCipherData();
		}

		private void InvalidateCipherData()
		{
			if (_key != null)
			{
				Array.Clear(_key, 0, _key.Length);
			}

			_deriveBytes.Reset();
			_key = _deriveBytes.GetBytes(_symCipher.KeySize / 8);
		}

		public Account CreateAccount(string name)
		{
			return new Account(this, name);
		}

		public Account ImportAccount(Account account)
		{
			if (account == null)
			{
				throw new ArgumentNullException("account");
			}

			return (Account) account.Clone(this);
		}

		private string VersionToString(DatabaseVersion version)
		{
			switch (version)
			{
			case DatabaseVersion.Version1_0:
				return "1.0";
			}

			/*Shouldn't reach here.*/
			throw new Exception();
		}

		private DatabaseVersion ParseVersion(XmlElement elem, string attributeName)
		{
			if (elem == null)
			{
				throw new ArgumentNullException("elem");
			}

			if (string.IsNullOrEmpty(attributeName))
			{
				throw new ArgumentException("Cannot be null or empty.", "attributeName");
			}

			string versionStr = elem.GetAttribute(attributeName);
			if (string.IsNullOrEmpty(versionStr))
			{
				throw new MissingAttributeException(elem.Name, attributeName);
			}

			versionStr = versionStr.Trim();
			if (versionStr.Equals("1.0"))
			{
				return DatabaseVersion.Version1_0;
			}

			throw new VersionNotSupportedException(versionStr);
		}

		public void Load(string fileName)
		{
			Load(fileName, true);
		}

		public void Load(string fileName,bool checkXmlSig)
		{
			if (fileName == null)
			{
				throw new ArgumentNullException("fileName");
			}

			/*Open the specified file for reading.
				 Make sure other processes cannot modify/delete the file
				 while we're reading it.*/
			using (FileStream stream = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
			{
				using (XmlReader reader = XmlReader.Create(stream, s_readerSettings))
				{
					XmlDocument doc = new XmlDocument();

					/*Ignore whitespace to speed up reading, and more importantly,
					 to enable our signature checking to function properly.*/
					doc.PreserveWhitespace = false;

					try
					{
						doc.Load(reader);
					}
					catch (XmlException e)
					{
						throw new XmlParseException(e);
					}

					this.Deserialize(doc.DocumentElement,checkXmlSig);
				}
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileName"></param>
		/// <exception cref="OutOfMemoryException"></exception>
		/// <exception cref="IOException"></exception>
		/// <remarks>This method can throw all kinds of file and IO related exceptions.
		/// See <see cref="File.Open"/> for more details.</remarks>
		public void Save(string fileName)
		{
			if (fileName == null)
			{
				throw new ArgumentNullException("fileName");
			}

			XmlWriter writer = null;
			FileStream stream = null;
			
			try
			{
				/*Creates a new file. Existing file will be overwritten,
					and does not share the file with other processes. */
				stream = File.Open(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
				writer = XmlWriter.Create(stream, s_writerSettings);

				XmlDocument doc = new XmlDocument();

				/*Strips out whitespaces.*/
				doc.PreserveWhitespace = false;

				XmlElement rootElem = doc.CreateElement(this.TagName);
				/*Note: must append the root element first before we compute the
				 signature in Serialize*/
				doc.AppendChild(rootElem);

				this.Serialize(rootElem);
				
				doc.Save(writer);
			}
			finally
			{
				/*'writer' will close the file stream for us.*/
				if (writer != null)
				{
					try
					{
						/*We have to wrap this call inside a try-catch block because 
						 it may throw an exception due to a possible malformed XML document
						 being written to the file.*/
						writer.Close();
					}
					catch
					{
						/*Ok, try to close the stream instead.*/
						try
						{
							stream.Close();
						}
						catch
						{
							/*Ok, heck it... there's nothing we can do.*/
						}
					}
				}
				else if (stream != null)
				{
					stream.Close();
				}
			}
		}

		internal byte[] Decrypt(byte[] encBytes, byte[] ivBytes)
		{
			if (ivBytes == null)
			{
				throw new ArgumentNullException("ivBytes");
			}

			if (encBytes == null)
			{
				throw new ArgumentNullException("encBytes");
			}

			try
			{
				ICryptoTransform decrypter = _symCipher.CreateDecryptor(_key, ivBytes);

				byte[]b= decrypter.TransformFinalBlock(encBytes, 0, encBytes.Length);

				ParserUtil.Dispose(decrypter);

				return b;
			}
			catch (Exception e)
			{
				throw new CipherException(false, e);
			}
		}

		internal byte[] Encrypt(byte[] data,out byte[] ivBytes)
		{
			if (data == null)
			{
				throw new ArgumentNullException("data");
			}

			try
			{
				byte[]newIvBytes = new byte[_symCipher.BlockSize / 8];
				RNG.GetBytes(newIvBytes);
				ivBytes = newIvBytes;

				ICryptoTransform encrypter = _symCipher.CreateEncryptor(_key, newIvBytes);
				
				byte[]b= encrypter.TransformFinalBlock(data, 0, data.Length);

				ParserUtil.Dispose(encrypter);

				return b;
			}
			catch (Exception e)
			{
				throw new CipherException(true, e);
			}
		}

		/// <summary>
		/// Gets or sets the hasher used to sign files saved by this object.
		/// </summary>
		/// <remarks>When retrieving, a copy of the <see cref="Hasher"/> object
		/// is returned. Modifying this copy will not modify the copy stored in this
		/// object. Callers are responsible to dispose the retrieved copy.</remarks>
		public Hasher Hasher
		{
			get
			{
				return (Hasher) _hasher.Clone(this);
			}
			set
			{
				if (value == null)
				{
					throw new ArgumentNullException("value");
				}

				Hasher newHasher = (Hasher) value.Clone(this);

				_hasher.Parent = null;
				ParserUtil.Dispose(_hasher);

				_hasher = newHasher;
				_hasher.Parent = this;
			}
		}

		/// <summary>
		/// Gets or sets the symmetric cipher used to encrypt data in
		/// files saved by this object.
		/// </summary>
		/// <remarks>When retrieving, a copy of the <see cref="SymmetricCipher"/> object
		/// is returned. Modifying this copy will not modify the copy stored in this
		/// object. Callers are responsible to dispose the retrieved copy.</remarks>
		public SymmetricCipher SymmetricCipher
		{
			get
			{
				return (SymmetricCipher) _symCipher.Clone(this);
			}
			set
			{
				if (value == null)
				{
					throw new ArgumentNullException("value");
				}
				
				/*Must ensure it's decrypted first...*/
				_accounts.EnsureDecrypted(true);

				SymmetricCipher newSymmetricCipher = (SymmetricCipher) value.Clone(this);

				_symCipher.Parent = null;
				ParserUtil.Dispose(_symCipher);

				/*...before we change the cipher.*/
				_symCipher = newSymmetricCipher;
				_symCipher.Parent = this;

				InvalidateCipherData();
			}
		}
		
		public AccountCollection Accounts
		{
			get
			{
				return _accounts;
			}
		}

		/// <summary>
		/// Gets the version this object is currently using.
		/// </summary>
		/// <remarks>This property reflects the version in 
		/// which files are saved in.</remarks>
		public DatabaseVersion Version
		{
			get
			{
				return _version;
			}
		}

		public override void Deserialize(XmlElement elem)
		{
			Deserialize(elem, true);
		}

		public void Deserialize(XmlElement elem, bool checkXmlSig)
		{
			if (elem == null)
			{
				throw new ArgumentNullException("elem");
			}

			Hasher hasher = new Hasher(this, HasherType.SHA512);
			SymmetricCipher symCipher = new SymmetricCipher(this, SymmetricCipherType.AES);
			AccountCollection accounts = null;
			DatabaseVersion version;
			bool ok = false;

			try
			{
			
				/*We need the hasher element to verify the signature.*/
				XmlElement hasherElem = ParserUtil.GetSingleChild(elem, hasher.TagName);
				hasher.Deserialize(hasherElem);

				if (checkXmlSig)
				{
					/*Check the signature as early as possible so that we can know
					 whether the supplied password is valid.*/
					XmlNodeList nodeList = elem.GetElementsByTagName(SignatureElementName);
					if (nodeList.Count == 1)
					{
						SignedXml signedXml = new SignedXml(elem.OwnerDocument);
						signedXml.LoadXml((XmlElement) nodeList[0]);

						HMAC keyedHasher = (HMAC) hasher.CreateKeyedHasher();
						_deriveBytes.Reset();
						keyedHasher.Key = _deriveBytes.GetBytes(HMACKeyLength);

						if (!signedXml.CheckSignature(keyedHasher))
						{
								throw new SignatureVerificationException(
									SignatureVerificationFailureReason.InvalidSignature
								);
						}
							
						ParserUtil.Dispose(keyedHasher);
					}
					else if (nodeList.Count == 0)
					{
						throw new SignatureVerificationException(SignatureVerificationFailureReason.MissingSignature);
					}
					else
					{
						throw new SignatureVerificationException(SignatureVerificationFailureReason.MultipleSignature);
					}
				}

				/*  the version does not affect the parsing for now.*/
				version = ParseVersion(elem, VersionAttributeName);

				XmlElement symCipherElem = ParserUtil.GetSingleChild(elem, symCipher.TagName);
				symCipher.Deserialize(symCipherElem);

				accounts = new AccountCollection(this);
				XmlElement accountsElem = ParserUtil.GetSingleChild(elem, accounts.TagName);
				accounts.Deserialize(accountsElem);

				ok = true;
			}
			finally
			{
				if (!ok)
				{
					ParserUtil.Dispose(symCipher);
					ParserUtil.Dispose(hasher);
				}
			}

			_accounts.Parent = null;
			_symCipher.Parent = null;
			_hasher.Parent = null;

			ParserUtil.Dispose(_symCipher);
			ParserUtil.Dispose(_hasher);

			/*Finally, everything's OK.
			 We put the assignment at the very last so that there will be no
			 * chance of an error occurring that disrupts the changing of internal
			 * data.
			 */

			_version = version;
			_hasher = hasher;
			_symCipher = symCipher;
			_accounts = accounts;

			_accounts.Parent = this;
			_symCipher.Parent = this;
			_hasher.Parent = this;
		}

		public override void Serialize(XmlElement elem)
		{
			if (elem == null)
			{
				throw new ArgumentNullException("elem");
			}

			elem.SetAttribute(VersionAttributeName, VersionToString(_version));
			
			XmlElement symmetricCipherElem = elem.OwnerDocument.CreateElement(_symCipher.TagName);
			_symCipher.Serialize(symmetricCipherElem);
			elem.AppendChild(symmetricCipherElem);

			XmlElement hasherElem = elem.OwnerDocument.CreateElement(_hasher.TagName);
			_hasher.Serialize(hasherElem);
			elem.AppendChild(hasherElem);

			XmlElement accountsElem = elem.OwnerDocument.CreateElement(_accounts.TagName);
			_accounts.Serialize(accountsElem);
			elem.AppendChild(accountsElem);

			SignedXml signedXml = new SignedXml(elem.OwnerDocument);
			Reference reference = new Reference();

			/*Sign the whole document.*/
			reference.Uri = "";
			reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
			signedXml.AddReference(reference);

			HMAC keyedHasher = (HMAC) _hasher.CreateKeyedHasher();
			_deriveBytes.Reset();
			keyedHasher.Key = _deriveBytes.GetBytes(HMACKeyLength);

			/*Note that 'keyedHasher' must be derived from HMAC.*/
			signedXml.ComputeSignature(keyedHasher);
			elem.AppendChild(elem.OwnerDocument.ImportNode(signedXml.GetXml(), true));

			ParserUtil.Dispose(keyedHasher);
		}

		protected internal override Element Clone(Database ownerDatabase)
		{
			throw new NotSupportedException();
		}

		public override Element Parent
		{
			get
			{
				return null;
			}
			protected internal set
			{
				throw new NotSupportedException();
			}
		}

		public override string TagName
		{
			get
			{
				return ElementName;
			}
		}

		#endregion
	}
}
